Ever have an Enum that you needed to deserialize different than it's value in json. I recently needed to do just that and since I use Spring Boot with Jackson a lot I went to find an example of how to do this online and found the examples lacking. So I decided to write this blog hopefully it helps someone else out. In this example the customer is sending you a version:
{
"version": "1.0.0"
}
But that is not a valid variable name in Java, so you'll either need to keep it as a string or represent it via enum which allows your code to be fully aware of exactly which versions are being read and you'll immediately know when your customer introduces a new version. Below you can see how I've defined the Java Enum representing versions:
@Log
@ToString
public enum Version {
VERSION_1("1.0.0"),
VERSION_1_0_2("1.0.2");
static Map<String, Version> VERSION_MAP;
static {
VERSION_MAP = Stream.of(Version.values())
.collect(toMap(Version::getVersion, Function.identity()));
}
String version;
Version(String version){
this.version = version;
}
public String getVersion(){
return version;
}
public static Version findVersionFromVersionString(String versionString){
Version version = VERSION_MAP.get(versionString);
log.info("Versions: " + VERSION_MAP);
if(version!=null)
return version;
throw new NoSuchElementException(versionString + " is not a proper version.");
}
}
In order to have Jackson natively do the conversion you'll have to write a custom deserializer. Below you can see a custom deserializer for the version enum:
/**
* Deserializer for Version
*/
@Log
public class JacksonVersionDeserializer extends StdDeserializer<Version> {
public JacksonVersionDeserializer(){
this(null);
}
protected JacksonVersionDeserializer(Class<?> vc) {
super(vc);
}
@Override
public Version deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
log.fine("Text " + node.asText());
return Version.findVersionFromVersionString(node.asText());
}
}
Testing
We can't say this works without a test. Below I have a parameterized test that ensures I can pass in all the version values via raw json and they can be converted to the enum.
@ParameterizedTest
@EnumSource(Version.class)
public void testWorks(Version xVersion) throws JsonProcessingException {
Version version = mapper.readValue("\"" + xVersion.version +"\"", Version.class);
log.info("Version: " + xVersion);
assertEquals(xVersion, version);
}
Bloopers
Where did I totally screw up trying to create the Deserializer and get this to work.
-
In the deserializer it's important to use node.asText() and not node.toString() because unless you've changed your toString() method it will likely print something like,
version=1.0.0which is not what you want to deserialize. -
Testing this I ran into two main issues:
- Expected space separating root-level values this was happening when I forgot to surround the String I was passing down to Jackson with quotes. You can see a test for this blooper below:
@Test public void testBlooperDeserializaerJsonParseException() { // Tried several different iterations of this JsonParseException exception = assertThrows(JsonParseException.class, () -> mapper.readValue(Version.VERSION_1_0_2.version, Version.class)); log.info(""+exception); }- non-static inner classes like this can only by instantiated using default, no-argument constructor I ran into this issue after getting frustrated with issue 1 and trying to just wrap the whole this in a POJO. You can see a test showing this issue below:
@Test public void testBlooperNeedCustomSerializerForThisToWork() throws JsonProcessingException { Example example = new Example(); example.setVersions(Arrays.asList(Version.VERSION_1)); InvalidDefinitionException exception = assertThrows(InvalidDefinitionException.class, () -> mapper.readValue(mapper.writeValueAsString(example), Example.class)); log.info("" + exception); }My true problem here is that I likely needed a custom serializer to properly write out my enum. Proving that thought is for another entry though.
To see the source code for the examples above visit my github here.